﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using LINQPad;
using LINQPad.Extensibility.DataContext;
using LINQPadDriverHelper;
using Microsoft.CSharp;

namespace LINQPadDriver
{
  public class LINQPadDriver : DynamicDataContextDriver, IDriver
  {
    private bool hasException;
    private ModelCacheContainer model;
    private ISession session;


    public override string Name
    {
      get { return "DataObjects.Net 5.0"; }
    }

    public override string Author
    {
      get { return "Alexander Ovchinnikov"; }
    }

    private ModelCacheContainer Model
    {
      get { return model ?? (model = ModelCache.GetModel(Helper.CxInfo)); }
    }

    public override bool AreRepositoriesEquivalent(IConnectionInfo c1, IConnectionInfo c2)
    {
      return ConnectionInfosEquals(c1, c2);
    }

    public override void ClearConnectionPools(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      base.ClearConnectionPools(cxInfo);
    }

    public override void DisplayObjectInGrid(object objectToDisplay, GridOptions options)
    {
      objectToDisplay = Helper.DisplayObjectInGrid(objectToDisplay);
      base.DisplayObjectInGrid(objectToDisplay, options);
    }


    public override void ExecuteESqlQuery(IConnectionInfo cxInfo, string query)
    {
      Helper.CxInfo = cxInfo;
      base.ExecuteESqlQuery(cxInfo, query);
    }

    public override string GetAppConfigPath(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return base.GetAppConfigPath(cxInfo);
    }

    public override IEnumerable<string> GetAssembliesToAdd(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      yield return Assembly.GetExecutingAssembly().Location;
      yield return Assembly.GetExecutingAssembly().Location.Replace("LINQPadDriver", "LINQPadDriverHelper");
      string assembly = Helper.FindAssembly("Xtensive.Core");
      if (assembly!=null)
        yield return assembly;
      assembly = Helper.FindAssembly("Xtensive.Orm");
      if (assembly!=null)
        yield return assembly;
      foreach (string path in Helper.Assemblies())
        yield return path;
      foreach (string a in Model.GetAssemblyNames()) {
        yield return Helper.FindAssembly(a);
      }
    }

    public override string GetConnectionDescription(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return cxInfo.DisplayName;
    }

    public override object[] GetContextConstructorArguments(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return new object[] {this};
    }

    public override ParameterDescriptor[] GetContextConstructorParameters(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return new[] {new ParameterDescriptor("driver", typeof (LINQPadDriver).FullName)};
    }

    public override ICustomMemberProvider GetCustomDisplayMemberProvider(object objectToWrite)
    {
      ICustomMemberProvider customDisplayMemberProvider = Helper.CustomDisplayMemberProvider(objectToWrite);
      if (customDisplayMemberProvider!=null)
        return customDisplayMemberProvider;
      return base.GetCustomDisplayMemberProvider(objectToWrite);
    }

    public override IDbConnection GetIDbConnection(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return base.GetIDbConnection(cxInfo);
    }

    public override DateTime? GetLastSchemaUpdate(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return base.GetLastSchemaUpdate(cxInfo);
    }

    public override IEnumerable<string> GetNamespacesToAdd(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      yield return "Xtensive.Orm";
      IEnumerable<string> namespases = Model.GetNamespaces();
      foreach (
        string n in
          namespases) {
        yield return n;
      }
    }

    public override IEnumerable<string> GetNamespacesToRemove(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return base.GetNamespacesToRemove(cxInfo);
    }

    public override DbProviderFactory GetProviderFactory(IConnectionInfo cxInfo)
    {
      Helper.CxInfo = cxInfo;
      return base.GetProviderFactory(cxInfo);
    }

    /// <summary>
    /// </summary>
    /// <param name="cxInfo">
    ///   The cx info.
    /// </param>
    /// <param name="assemblyToBuild">
    ///   The assembly to build.
    /// </param>
    /// <param name="nameSpace">
    ///   The name space.
    /// </param>
    /// <param name="typeName">
    ///   The type name.
    /// </param>
    /// <returns>
    /// </returns>
    /// <exception cref="Exception">
    /// </exception>
    public override List<ExplorerItem> GetSchemaAndBuildAssembly(
      IConnectionInfo cxInfo,
      AssemblyName assemblyToBuild,
      ref string nameSpace,
      ref string typeName)
    {
      Helper.CxInfo = cxInfo;
      Helper.CxInfo.SessionData["assembly"] = assemblyToBuild;
      CompilerResults results;
      using (var codeProvider = new CSharpCodeProvider(new Dictionary<string, string> {{"CompilerVersion", "v4.0"}})) {
        var options = new CompilerParameters(new string[0], assemblyToBuild.CodeBase, false);
        options.ReferencedAssemblies.Add("System.dll");
        options.ReferencedAssemblies.Add("System.Core.dll");
        options.ReferencedAssemblies.Add("System.Data.Linq.dll");
        options.ReferencedAssemblies.Add(typeof (LINQPadDriver).Assembly.Location);
        options.ReferencedAssemblies.Add(typeof (Helper).Assembly.Location);
        options.ReferencedAssemblies.Add(Helper.FindAssembly("Xtensive.Orm"));
        foreach (string assembly in Model.GetAssemblyNames()) {
          options.ReferencedAssemblies.Add(Helper.FindAssembly(assembly));
        }
        options.CompilerOptions = "/optimize";
        string code = Model.GetWrappers();
        results = codeProvider.CompileAssemblyFromSource(options, new[] {code});
      }

      if (results.Errors.Count > 0)
        throw new Exception(
          "Cannot compile typed context: " + results.Errors[0].ErrorText + " (line " + results.Errors[0].Line + ")");

      nameSpace = "LINQPadDriverHelper";
      typeName = "LINQPadSession";

      try {
        return GetSchema();
      }
      catch (Exception e) {
        if (e is TargetInvocationException)
          e = e.InnerException;
        return new List<ExplorerItem> {
          new ExplorerItem("Exception (drop to query window to view)", ExplorerItemKind.Category, ExplorerIcon.Box)
          {DragText = e.ToString()}
        };
      }
    }


    public override void InitializeContext(
      IConnectionInfo cxInfo,
      object context,
      QueryExecutionManager executionManager)
    {
      Helper.CxInfo = cxInfo;
      Helper.SQLLog = new StringBuilder();
      session = (ISession) context;
      hasException = false;
      Helper.DataContext = new DataContextHack();
      typeof(Util).GetProperty("CurrentDataContext").SetValue(null, Helper.DataContext, new object[0]);
      string path = null;
      if (!string.IsNullOrWhiteSpace(Helper.CxInfo.AppConfigPath))
        path = Path.GetDirectoryName(Helper.CxInfo.AppConfigPath);
      DriverConfig config = DriverConfig.Get(Helper.CxInfo);
      if (path==null && config.Assemblies.Count > 0)
        path = Path.GetDirectoryName(config.Assemblies[0]);
      if (path!=null)
        Directory.SetCurrentDirectory(path);
      base.InitializeContext(cxInfo, context, executionManager);
    }

    public override void OnQueryFinishing(
      IConnectionInfo cxInfo,
      object context,
      QueryExecutionManager executionManager)
    {
      session = (ISession) context;
      Helper.OnQueryFinishing(cxInfo, context, executionManager);

      base.OnQueryFinishing(cxInfo, context, executionManager);
    }


    public override void PreprocessObjectToWrite(ref object objectToWrite, ObjectGraphInfo info)
    {
      if (!info.ParentHierarchy.Any()) {
        Helper.CleanCache();
        if (objectToWrite is Exception)
          hasException = true;
      }
      base.PreprocessObjectToWrite(ref objectToWrite, info);
    }

    public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection)
    {
      Helper.CxInfo = cxInfo;
      return new ConnectionDialog(cxInfo).ShowDialog()==true;
    }

    public override void TearDownContext(
      IConnectionInfo cxInfo,
      object context,
      QueryExecutionManager executionManager,
      object[] constructorArguments)
    {
      Helper.CxInfo = cxInfo;
      session = (ISession) context;
      EndTransaction();
      base.TearDownContext(cxInfo, context, executionManager, constructorArguments);
    }

    #region Non-public methods

    private static bool ConnectionInfosEquals(IConnectionInfo c1, IConnectionInfo c2)
    {
      if (ReferenceEquals(c1, c2))
        return true;
      if (c1==null || c2==null)
        return false;
      return c1.DriverData.ToString()==c2.DriverData.ToString() && c1.AppConfigPath==c2.AppConfigPath;
    }


    private void EndTransaction()
    {
      if (!hasException)
        session.Commit();
      session.Dispose();
    }

    private List<ExplorerItem> GetSchema()
    {
      List<ExplorerItem> items = Model.GetExploreItems();
      return items;
    }

    #endregion

    static LINQPadDriver()
    {
      //remove LINQPad handler
      FieldInfo field = typeof (AppDomain).GetField("_AssemblyResolve", BindingFlags.NonPublic | BindingFlags.Instance);
      Debug.Assert(field!=null, "field != null");
      Helper.LINQPadAssemblyResolveHandler = (ResolveEventHandler) field.GetValue(AppDomain.CurrentDomain);
      field.SetValue(AppDomain.CurrentDomain, null);
      AppDomain.CurrentDomain.AssemblyResolve += Helper.CurrentDomainOnAssemblyResolve;
    }
    public void LogExpression(string expression, TimeSpan time)
    {
      if (Helper.SQLLog==null)
        return;
      Helper.SQLLog.AppendLine("-- Region linq time: " + time.TotalMilliseconds + " ms");
      Helper.SQLLog.AppendLine(expression);
      Helper.SQLLog.AppendLine("-- EndRegion");
    }

    public void LogOperation(string name, string description, TimeSpan time)
    {
      if (Helper.SQLLog == null)
        return;
      Helper.SQLLog.AppendLine("-- Region DO operation: " + name + ", time: " + time.TotalMilliseconds + " ms");
      Helper.SQLLog.AppendLine(description);
      Helper.SQLLog.AppendLine("-- EndRegion");
    }

    public void LogSql(string command, TimeSpan time, IEnumerable<Tuple<string, string>> warnings)
    {
      if (Helper.SQLLog == null)
        return;
      if (warnings!=null)
        foreach (var warning in warnings) {
          Helper.SQLLog.AppendLine("-- Region " + warning.Item1);
          IEnumerable<string> lines =
            warning.Item2.Split(new[] {Environment.NewLine}, StringSplitOptions.None).Select(a => "-- " + a);
          Helper.SQLLog.AppendLine(string.Join(Environment.NewLine, lines));
          Helper.SQLLog.AppendLine("-- EndRegion");
        }
      Helper.SQLLog.Append(command);
      Helper.SQLLog.AppendLine("-- sql time: " + time.TotalMilliseconds + " ms");
    }

    public string FormatSql(string text)
    {
      return new CommandFormatter(text).Format();
    }

  }
}